/*
 * @(#)PhotoAlbum.java	1.23 02/09/10 @(#)
 *
 * Copyright (c) 2001-2002 Sun Microsystems, Inc.  All rights reserved.
 * PROPRIETARY/CONFIDENTIAL
 * Use is subject to license terms.
 */

package example.photoalbum;

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
import java.util.Vector;
import java.io.IOException;
import java.io.DataInputStream;
import javax.microedition.io.HttpConnection;
import javax.microedition.io.ContentConnection;
import javax.microedition.io.Connector;

import javax.microedition.rms.*;

import example.About;

/**
 * The PhotoAlbum MIDlet provides the commands and screens
 * that implement a simple photograph and animation album.
 * The images and animations to be displayed are configured
 * in the descriptor file with attributes.
 * <p>
 * Options are provided to to vary the speed of display
 * and the frame style.
 *
 */
public class PhotoAlbum
    extends MIDlet
    implements CommandListener, ItemStateListener, Runnable
{
    /** The Command object for the About command */
    private Command aboutCommand;

    /** The Command object for the Exit command */
    private Command exitCommand;
    /** The Command object for the Ok command */
    private Command okCommand;
    /** The Command object for the Options command */
    private Command optionsCommand;
    /** The Command object for the Back command */
    private Command backCommand;
    /** The Command object for the Cancel command */
    private Command cancelCommand;

    /** The Form object for the Progress form */
    private Form progressForm;
    /** The Gauge object for the Progress gauge */
    private Gauge progressGauge;

    /** The Form object for the Options command */
    private Form optionsForm;

    /** Set of choices for the border styles */
    private ChoiceGroup borderChoice;

    /** Set of choices for the speeds */
    private ChoiceGroup speedChoice;

    /** The current display for this MIDlet */
    private Display display;
    /** The PhotoFrame that displays images */
    private PhotoFrame frame;
    /** The Alert for messages */
    private Alert alert;
    /** Contains Strings with the image names */
    private Vector imageNames;

    /** List of Image titles for user to select */
    private List imageList;
    /** Name of current image, may be null */
    private String imageName;
    /** Current thread loading images, may be null */
    private Thread thread;

    /** Name of persistent storage */
    private final String optionsName = "PhotoAlbum";
    /** Persistent storage for options */
    private RecordStore optionsStore;

    /**
     * Construct a new PhotoAlbum MIDlet and initialize the base options
     * and main PhotoFrame to be used when the MIDlet is started.
     */
    public PhotoAlbum() {  
        display = Display.getDisplay(this);
        exitCommand = new Command("Exit", Command.EXIT, 1);
	optionsCommand = new Command("Options", Command.SCREEN, 1);
	okCommand = new Command("Ok", Command.OK, 3);
        backCommand = new Command("Back", Command.BACK, 3);
        cancelCommand = new Command("Cancel", Command.CANCEL, 1);
	aboutCommand = new Command("About", Command.HELP, 30);

        frame = new PhotoFrame();
	frame.setStyle(2);
	frame.setSpeed(2);
	frame.addCommand(optionsCommand);
        frame.addCommand(backCommand);
        frame.setCommandListener(this);
        alert = new Alert("Warning");
        setupImageList();
    }

    /**
     * Start up the Hello MIDlet by setting the PhotoFrame
     * and loading the initial images.
     */
    protected void startApp() {
        if (imageList.size() > 0) {
            display.setCurrent(imageList);
            openOptions();
            restoreOptions();
        } else {
            alert.setString("No images configured.");
            display.setCurrent(alert, imageList);
        }
    }

    /**
     * Pause is used to release the memory used by Image.
     * When restarted the images will be re-created.
     * Save the options for the next restart.
     */
    protected void pauseApp() {
        saveOptions();
        frame.reset();          // Discard images cached in the frame.
    }

    /**
     * Destroy must cleanup everything not handled by the garbage collector.
     * In this case there is nothing to cleanup.
     * Save the options for the next restart.
     * @param unconditional true if this MIDlet should always cleanup
     */
    protected void destroyApp(boolean unconditional) {
        saveOptions();
        frame.reset();          // Discard images cached in the frame.
        saveOptions();
        closeOptions();
    }

    /**
     * Respond to commands. Commands are added to each screen as
     * they are created.  Each screen uses the PhotoAlbum MIDlet as the 
     * CommandListener. All commands are handled here:
     * <UL>
     * <LI>Select on Image List - display the progress form and start the thread
     *  to read in the images.
     * <LI>Options - display the options form.
     * <LI>Ok on the Options form - returns to the PhotoFrame.
     * <LI>Back - display the Image List, deactivating the PhotoFrame.
     * <LI>Cancel - display the image List and stop the thread loading images.
     * <LI>Exit - images are released and notification is given that the MIDlet
     *  has exited.
     * </UL>
     * @param c the command that triggered this callback
     * @param s the screen that contained the command
     */
    public void commandAction(Command c, Displayable s) {
        if (c == exitCommand) {
            // Cleanup and notify that the MIDlet has exited
            destroyApp(false);
            notifyDestroyed();
        } else if (c == optionsCommand) {
            // Display the options form
	    display.setCurrent(genOptions());
	} else if (c == okCommand && s == optionsForm) {
            // Return to the PhotoFrame, the option values have already
            // been saved by the item state listener
	    display.setCurrent(frame);
	} else if (c == List.SELECT_COMMAND) {
            // Display the progress screen and
            // start the thread to read the images
            int i = imageList.getSelectedIndex();
            imageName = (String)imageNames.elementAt(i);
	    display.setCurrent(genProgress(imageList.getString(i)));
	    thread = new Thread(this);
            thread.start();
        } else if (c == backCommand) {
            // Display the list of images.
            display.setCurrent(imageList);
        } else if (c == cancelCommand) {
	    // Signal thread to stop and put an alert.
	    thread = null;
            alert.setString("Loading images cancelled.");
            display.setCurrent(alert, imageList);
	} else if (c == aboutCommand) {
	    About.showAbout(display);
	}
    }

    /**
     * Listener for changes to options.
     * The new values are set in the PhotoFrame.
     * @param item - the item whose value has changed.
     */
    public void itemStateChanged(Item item) {
	if (item == borderChoice) {
            frame.setStyle(borderChoice.getSelectedIndex());
	} else if (item == speedChoice) {
	    frame.setSpeed(speedChoice.getSelectedIndex());
	}
    }

    /**
     * Generate the options form with speed and style choices.
     * Speed choices are stop, slow, medium, and fast.
     * Style choices for borders are none, plain, fancy.
     * @return the generated options Screen
     */
    private Screen genOptions() {
	if (optionsForm == null) {
	    optionsForm = new Form("Options");
	    optionsForm.addCommand(okCommand);
	    optionsForm.setCommandListener(this);
	    optionsForm.setItemStateListener(this);

	    speedChoice = new ChoiceGroup("Speed", Choice.EXCLUSIVE);
	    speedChoice.append("Stop", null);
	    speedChoice.append("Slow", null);
	    speedChoice.append("Medium", null);
	    speedChoice.append("Fast", null);
	    speedChoice.append("Unlimited", null);
	    speedChoice.setSelectedIndex(frame.getSpeed(), true);
	    optionsForm.append(speedChoice);

	    borderChoice = new ChoiceGroup("Borders", Choice.EXCLUSIVE);
	    borderChoice.append("None", null);
	    borderChoice.append("Plain",  null);
	    borderChoice.append("Fancy", null);
	    borderChoice.setSelectedIndex(frame.getStyle(), true);
	    optionsForm.append(borderChoice);
	}
	return optionsForm;
    }
    
    /**
     * Generate the options form with image title and progress gauge.
     * @param name the title of the Image to be loaded.
     * @return the generated progress screen
     */
    private Screen genProgress(String name) {
	if (progressForm == null) {
	    progressForm = new Form(name);
	    progressForm.addCommand(cancelCommand);
	    progressForm.setCommandListener(this);

	    progressGauge =
		new javax.microedition.lcdui.Gauge("Loading images...",
						   false, 9, 0);

	    progressForm.append(progressGauge);
	} else {
	    progressGauge.setValue(0);
	    progressForm.setTitle(name);
	}
	return progressForm;
    }

    /**
     * Check the attributes in the descriptor that identify
     * images and titles and initialize the lists of imageNames
     * and imageList.
     * <P>
     * The attributes are named "PhotoTitle-n" and "PhotoImage-n".
     * The value "n" must start at "1" and increment by 1.
     */
    private void setupImageList() {
        imageNames = new Vector();
        imageList = new List("Images", List.IMPLICIT);
        imageList.addCommand(exitCommand);
	imageList.addCommand(aboutCommand);
        imageList.setCommandListener(this);

	for (int n = 1; n < 100; n++) {
	    String nthImage = "PhotoImage-"+ n;
	    String image = getAppProperty(nthImage);
	    if (image == null || image.length() == 0)
		break;

            String nthTitle = "PhotoTitle-" + n;
            String title = getAppProperty(nthTitle);
            if (title == null || title.length() == 0)
		title = image;

            imageNames.addElement(image);
            imageList.append(title, null);
	}
        imageNames.addElement("testchart:");
        imageList.append("Test Chart", null);
    }

    /**
     * The Run method is used to load the images.
     * A form is used to report the progress of loading images
     * and when the loading is complete they are displayed.
     * Any errors that occur are reported using an Alert.
     * Images previously loaded into the PhotoFrame are discarded
     * before loading.
     * <P>
     * Load images from resource files using <code>Image.createImage</code>.
     * Images may be in resource files or accessed using http:
     * The first image is loaded to determine whether it is a
     * single image or a sequence of images and to make sure it exists.
     * If the name given is the complete name of the image then
     * it is a singleton.
     * Otherwise it is assumed to be a sequence of images 
     * with the name as a prefix.  Sequence numbers (n) are
     * 0, 1, 2, 3, ....  The full resource name is the concatenation
     * of name + n + ".png".
     * <p>
     * If an OutOfMemoryError occurs the sequence of images is truncated
     * and an alert is used to inform the user. The images loaded are
     * displayed.
     * @see createImage
     */
    public void run() {
	Thread mythread = Thread.currentThread();
        Vector images = new Vector(5);
	/* Free images and resources used by current frame. */
        frame.reset();
        try {			// Catch OutOfMemory Errors
	    try {
                if (imageName.startsWith("testchart:")) {
                    TestChart t = new TestChart(frame.getWidth(),
                                                    frame.getHeight());
                    images = t.generateImages();
                } else {
                    // Try the name supplied for the single image case.
		    images.addElement(createImage(imageName));
                }
	    } catch (IOException ex) {
		try {
		    int namelen = imageName.length();
		    StringBuffer buf = new StringBuffer(namelen + 8);
		    buf.append(imageName);
		    Runtime rt = Runtime.getRuntime();
		    // Try for a sequence of images.
		    for (int i = 0; ; i++) {

			progressGauge.setValue(i % 10);
                        // If cancelled, discard images and return immediately
			if (thread != mythread) {
			    break;
			}

			// locate the next in the series of images.
			buf.setLength(namelen);
			buf.append(i);
			buf.append(".png");
			String name = buf.toString();
			images.addElement(createImage(name));
		    }
		} catch (IOException io_ex) {
		}
	    } catch (SecurityException se_ex) {
		// no-retry, just put up the alert
	    }

            // If cancelled, discard images and return immediately
            if (thread != mythread) {
                return;
            }

            // If any images, setup the images and display them.
	    if (images.size() > 0) {
                frame.setImages(images);
		display.setCurrent(frame);
	    } else {
		// Put up an alert saying image cannot be loaded
		alert.setString("Images could not be loaded.");
		display.setCurrent(alert, imageList);
	    }
	    
	} catch (OutOfMemoryError err) {
	    int size = images.size();
	    if (size > 0) {
		images.setSize(size-1);
	    }

	    // If cancelled, discard images and return immediately
	    if (thread != mythread) {
		return;
	    }

	    alert.setString("Not enough memory for all images.");
            // If no images are loaded, Alert and return to the list
	    // Othersize, Alert and display the ones that were loaded.
	    if (images.size() <= 0) {
 		display.setCurrent(alert, imageList);
	    } else {
                frame.setImages(images);
                display.setCurrent(alert, frame);
            }
        }
    }

    /**
     * Fetch the image.  If the name begins with "http:"
     * fetch it with connector.open and http. 
     * If it starts with "/" then load it from the
     * resource file.
     * @param name of the image to load
     * @return image created
     * @exception IOException if errors occuring doing loading
     */
    private Image createImage(String name) throws IOException {
        if (name.startsWith("/")) {
            // Load as a resource with Image.createImage
            return Image.createImage(name);
        } else if (name.startsWith("http:")) {
            // Load from a ContentConnection
            HttpConnection c = null;
	    DataInputStream is = null;
            try {
	        c = (HttpConnection)Connector.open(name);
		int status = c.getResponseCode();
		if (status != 200) {
		    throw new IOException("HTTP Response Code = " + status);
		}

                int len = (int)c.getLength();
                String type = c.getType();
		if (!type.equals("image/png")) {
		    throw new IOException("Expecting image, received " + type);
		}

                if (len > 0) {
		    is = c.openDataInputStream();
		    byte[] data = new byte[len];
		    is.readFully(data);
                    return Image.createImage(data, 0, len);
	        } else {
		    throw new IOException("Content length is missing");
	        }
	    } finally {
	        if (is != null)
                    is.close();
                if (c != null)
                    c.close();
            }
        } else {
            throw new IOException("Unsupported media");
        }
    }

    /**
     * Open the store that holds the saved options.
     * If an error occurs, put up an Alert.
     */
    void openOptions() {
        try {
            optionsStore = RecordStore.openRecordStore(optionsName, true);
        } catch (RecordStoreException ex) {
            alert.setString("Could not access options storage");
            display.setCurrent(alert);
            optionsStore = null;
        }
    }
    
    /**
     * Save the options to persistent storage.
     * The options are retrieved ChoiceGroups and stored
     * in Record 1 of the store which is reserved for it.
     * The two options are stored in bytes 0 and 1 of the record.
     */
    void saveOptions() {
        if (optionsStore != null) {
            byte[] options = new byte[2];
            options[0] = (byte)frame.getStyle();
            options[1] = (byte)frame.getSpeed();
            try {
                optionsStore.setRecord(1, options, 0, options.length);
            } catch (InvalidRecordIDException ridex) {
                // Record 1 did not exist, create a new record (Should be 1)
                try {
                    int rec = optionsStore.addRecord(options,
						     0, options.length);
                } catch (RecordStoreException ex) {
                    alert.setString("Could not add options record");
                    display.setCurrent(alert);
                }
            } catch (RecordStoreException ex) {
                alert.setString("Could not save options");
                display.setCurrent(alert);
            }
        }
    }
    
    /**
     * Restore the options from persistent storage.
     * The options are read from record 1 and set in
     * the frame and if the optionsForm has been created
     * in the respective ChoiceGroups.
     */
    void restoreOptions() {
        if (optionsStore != null) {
            try {
                byte[] options = optionsStore.getRecord(1);
                if (options.length == 2) {
                    frame.setStyle(options[0]);
                    frame.setSpeed(options[1]);
                    if (optionsForm != null) {
                        borderChoice.setSelectedIndex(options[0], true);
                        speedChoice.setSelectedIndex(options[1], true);
                    }
                    return;         // Return all set
                } 
            } catch (RecordStoreException ex) {
		// Ignore, use normal defaults
            }
        }
    }
    
    /**
     * Close the options store.
     */
    void closeOptions() {
        if (optionsStore != null) {
            try {
                optionsStore.closeRecordStore();
                optionsStore = null;
            } catch (RecordStoreException ex) {
                alert.setString("Could not close options storage");
                display.setCurrent(alert);
            }
        }
    }
}
